﻿using System.Linq;
using System.Diagnostics;
namespace System.Collections.Generic
{
    public static class EnumeratorExtensions
    {
        /// <summary>
        /// Performs the specified action on each element of the IEnumerable&lt;T&gt;.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">The source list of items.</param>
        /// <param name="action">The System.Action&lt;T&gt; delegate to perform on each element of the IEnumerable&lt;T&gt;.</param>
        public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (action == null)
                throw new ArgumentNullException("action");

            using (IEnumerator<T> data = source.GetEnumerator())
                while (data.MoveNext())
                {
                    action(data.Current);
                }
        }

        /// <summary>
        /// Performs the specified action on each element of the IEnumerable&lt;T&gt;.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">The source list of items.</param>
        /// <param name="action">The System.Action&lt;T&gt; delegate to perform on each element of the IEnumerable&lt;T&gt;.</param>
        public static void ForEach<T>(this IEnumerable<T> source, Action<T, int> action)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (action == null)
                throw new ArgumentNullException("action");

            int index = 0;
            using (IEnumerator<T> data = source.GetEnumerator())
                while (data.MoveNext())
                {
                    action(data.Current, index);
                    index++;
                }
        }

        /// <summary>
        /// Groups items into same size lots.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">The source list of items.</param>
        /// <param name="size">The maximum size of the groups to make.</param>
        /// <returns>A list of list of items, where each list of items is no bigger than the size given.</returns>
        public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (size < 1)
                throw new ArgumentOutOfRangeException("size", "size must be greater than 0");

            return ClumpIterator<T>(source, size);
        }

        private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size)
        {
            Debug.Assert(source != null, "source is null.");

            T[] items = new T[size];
            int count = 0;
            foreach (var item in source)
            {
                items[count] = item;
                count++;

                if (count == size)
                {
                    yield return items;
                    items = new T[size];
                    count = 0;
                }
            }
            if (count > 0)
            {
                if (count == size)
                    yield return items;
                else
                {
                    T[] tempItems = new T[count];
                    Array.Copy(items, tempItems, count);
                    yield return tempItems;
                }
            }
        }

        /// <summary>
        /// Creates a list by applying a delegate to pairs of items in the IEnumerable&lt;T&gt;.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="TResult"></typeparam>
        /// <param name="source">The source list of items.</param>
        /// <param name="zipFunction">The delegate to use to combine items.</param>
        /// <returns></returns>
        public static IEnumerable<TResult> Scan<T, TResult>(this IEnumerable<T> source, Func<T, T, TResult> combine)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (combine == null)
                throw new ArgumentNullException("combine");

            return ScanIterator<T, TResult>(source, combine);
        }

        private static IEnumerable<TResult> ScanIterator<T, TResult>(IEnumerable<T> source, Func<T, T, TResult> combine)
        {
            Debug.Assert(source != null, "source is null.");
            Debug.Assert(combine != null, "combine is null.");

            using (IEnumerator<T> data = source.GetEnumerator())
                if (data.MoveNext())
                {
                    T first = data.Current;

                    while (data.MoveNext())
                    {
                        yield return combine(first, data.Current);
                        first = data.Current;
                    }
                }
        }

        /// <summary>
        /// Returns true if there are enough items in the IEnumerable&lt;T&gt;.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">The source list of items.</param>
        /// <param name="number">The number of items to check for.</param>
        /// <returns>True if there are enough items, false otherwise.</returns>
        public static bool AtLeast<T>(this IEnumerable<T> source, int number)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            int count = 0;
            using (IEnumerator<T> data = source.GetEnumerator())
                while (count < number && data.MoveNext())
                {
                    count++;
                }
            return count == number;
        }

        /// <summary>
        /// Returns true if there are enough items in the IEnumerable&lt;T&gt; to satisfy the condition.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">The source list of items.</param>
        /// <param name="number">The number of items to check for.</param>
        /// <param name="predicate">The condition to apply to the items.</param>
        /// <returns>True if there are enough items, false otherwise.</returns>
        public static bool AtLeast<T>(this IEnumerable<T> source, int number, Func<T, bool> condition)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (condition == null)
                throw new ArgumentNullException("condition");

            int count = 0;
            using (IEnumerator<T> data = source.GetEnumerator())
                while (count < number && data.MoveNext())
                {
                    if (condition(data.Current))
                        count++;
                }
            return count == number;
        }

        /// <summary>
        /// Returns true if there are no more than a set number of items in the IEnumerable&lt;T&gt;.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">The source list of items.</param>
        /// <param name="number">The number of items that must exist.</param>
        /// <returns>True if there are no more than the number of items, false otherwise.</returns>
        public static bool AtMost<T>(this IEnumerable<T> source, int number)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            bool result;
            int count = 0;
            using (IEnumerator<T> data = source.GetEnumerator())
            {
                while (count < number && data.MoveNext())
                {
                    count++;
                }
                result = !data.MoveNext();
            }
            return result;
        }

        /// <summary>
        /// Returns true if there are no more than a set number of items in the IEnumerable&lt;T&gt; that satisfy a given condition.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">The source list of items.</param>
        /// <param name="number">The number of items that must exist.</param>
        /// <param name="predicate">The condition to apply to the items.</param>
        /// <returns>True if there are no more than the number of items, false otherwise.</returns>
        public static bool AtMost<T>(this IEnumerable<T> source, int number, Func<T, bool> condition)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (condition == null)
                throw new ArgumentNullException("condition");

            bool result;
            int count = 0;
            using (IEnumerator<T> data = source.GetEnumerator())
            {
                while (count < number && data.MoveNext())
                {
                    if (condition(data.Current))
                        count++;
                }
                result = !data.MoveNext();
            }
            return result;
        }

        /// <summary>
        /// Creates a list by combining two other lists into one.
        /// </summary>
        /// <typeparam name="T1"></typeparam>
        /// <typeparam name="T2"></typeparam>
        /// <typeparam name="TResult"></typeparam>
        /// <param name="source1">One of the lists to zip.</param>
        /// <param name="source2">One of the lists to zip.</param>
        /// <param name="zipFunction">The delegate used to combine items.</param>
        /// <returns>A new list with the combined items.</returns>
        public static IEnumerable<TResult> Zip<T1, T2, TResult>(this IEnumerable<T1> source1, IEnumerable<T2> source2, Func<T1, T2, TResult> combine)
        {
            if (source1 == null)
                throw new ArgumentNullException("source1");
            if (source2 == null)
                throw new ArgumentNullException("source2");
            if (combine == null)
                throw new ArgumentNullException("combine");

            return ZipIterator<T1, T2, TResult>(source1, source2, combine);
        }

        private static IEnumerable<TResult> ZipIterator<T1, T2, TResult>(IEnumerable<T1> source1, IEnumerable<T2> source2, Func<T1, T2, TResult> combine)
        {
            Debug.Assert(source1 != null, "source1 is null.");
            Debug.Assert(source2 != null, "source2 is null.");
            Debug.Assert(combine != null, "combine is null.");

            using (IEnumerator<T1> data1 = source1.GetEnumerator())
            using (IEnumerator<T2> data2 = source2.GetEnumerator())
                while (data1.MoveNext() && data2.MoveNext())
                {
                    yield return combine(data1.Current, data2.Current);
                }
        }

        /// <summary>
        /// Creates a list by repeating another list.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">The list to repeat.</param>
        /// <param name="count">The number of items to return.</param>
        /// <returns>A circular list of items.</returns>
        public static IEnumerable<T> Cycle<T>(this IEnumerable<T> source, int count)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            return CycleIterator<T>(source, count);
        }

        private static IEnumerable<T> CycleIterator<T>(IEnumerable<T> source, int count)
        {
            Debug.Assert(source != null, "source is null.");

            int i = 0;
            while (i < count)
                using (IEnumerator<T> data = source.GetEnumerator())
                {
                    while (i < count && data.MoveNext())
                    {
                        yield return data.Current;
                        i++;
                    }
                }
        }

    }
}
